package org.jinghouyu.http.proxy.server;

import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.net.InetAddress;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.Set;

import org.jinghouyu.http.proxy.exception.HttpException;
import org.jinghouyu.http.proxy.stream.MergedInputStream;
import org.jinghouyu.http.proxy.utils.ParseUtils;

/**
 * request from client
 * @author liujingyu
 *
 */
public class Request implements Closeable {

	private LinkedHashMap<String, String> headers = new LinkedHashMap<>();
	private LinkedHashMap<String, String> cookies = new LinkedHashMap<>();
	private String messageHeader;
	private InputStream bodyInput;
	private InetAddress targetAddress;
	private Integer targetPort;

	private String clientHost;
	
	public String getUrl() {
		return messageHeader.split(" ")[1];
	}
	
	public boolean isSsl() {
		String url = getUrl();
		if(url.startsWith("https")) return true;
		return false;
	}
	
	public boolean isKeepAlive() {
		String connection = getHeader("Connection");
		return connection != null && "keep-alive".equalsIgnoreCase(connection);
	}
	
	public String getDomain() {
		String url = getUrl();
		String s = "://";
		int index = url.indexOf(s);
		url = url.substring(index + s.length());
		int end = url.indexOf("/");
		return end < 0 ? url : url.substring(0, end);
	}
	
	public String getHost() {
		String domain = getDomain();
		if(domain.contains(":")) {
			return domain;
		} else {
			return domain + ":" + (isSsl() ? 443 : 80);
		}
	}
	
	public InetAddress getHostAddress() throws UnknownHostException {
		if(targetAddress == null) {
			String host = getHost();
			String hostUrl = host.split(":")[0];
			targetAddress = InetAddress.getByName(hostUrl);
		}
		return targetAddress;
	}
	
	public int getPort() {
		if(targetPort == null) {
			String host = getHost();
			return targetPort = Integer.parseInt(host.split(":")[1]);
		}
		return targetPort;
	}
	
	public void setTargetAddress(String address) throws UnknownHostException {
		this.targetAddress = InetAddress.getByName(address);
	}
	
	public void setTargetPort(int targetPort) {
		this.targetPort = targetPort;
	}
	
	public String getMethod() {
		return messageHeader.split(" ")[0];
	}
	
	public String getProtocol() {
		return messageHeader.split(" ")[2];
	}
	
	private boolean add(String headerStr) {
		int index = headerStr.indexOf(":");
		if(index <= 0) return false;
		String name = headerStr.substring(0, index).trim();
		String value = headerStr.substring(index + 1).trim();
		if("Proxy-Connection".equalsIgnoreCase(name)) {
			name = "Connection";
		}
		headers.put(name, value);
		return true;
	}
	
	public String getHeader(String name) {
		if("Cookie".equals(name)) return null;
		return headers.get(name);
	}
	
	private void setClientHost(String clientHost) {
		this.clientHost = clientHost;
	}
	
	public String getClientHost() {
		return this.clientHost;
	}
	
	public InputStream getBodyInput() {
		return bodyInput;
	}
	
	private void parse_(Socket socket) throws IOException {
		InputStream in = socket.getInputStream();
		byte[] firstLine = ParseUtils.readLine(in);
		if(firstLine.length == 0) {
			throw new HttpException("heart-beaten test from client side");
		} 
		messageHeader = new String(firstLine);
		parseHeaders(in);
		bodyInput = in;
		InetAddress address = socket.getInetAddress();
		setClientHost(address.getHostAddress());
	}
	
	private void parseCookies() {
		String cookieStr = headers.get("Cookie");
		if(cookieStr == null) return;
		String[] cookiesString = cookieStr.split("; ");
		for(String cookie : cookiesString) {
			String name = cookie.split("=")[0];
			String value = null;
			if(cookie.split("=").length == 2) {
				value = cookie.split("=")[1];
			} else {
				value = "";
			}
			cookies.put(name, value);
		}
	}
	
	private void parseHeaders(InputStream in) throws IOException {
		while(true) {
			byte[] line = ParseUtils.readLine(in);
			if(line.length == 0) {
				break;
			}
			add(new String(line));
		}
		parseCookies();
	}
	
	public static Request parse(Socket socket) throws IOException {
		Request request = new Request();
		request.parse_(socket);
		return request;
	}
	
	private static final String CRLF = "\r\n";
	
	public String messageHeadersToString() {
		StringBuilder sb = new StringBuilder();
		String url = getUrl();
		if(url.contains("://")) {
			String indexStr = "://";
			url = url.substring(url.indexOf(indexStr) + indexStr.length());
			if(!url.contains("/")) {
				url = "/";
			} else {
				url = url.substring(url.indexOf("/"));
			}
		}
		return sb.append(getMethod())
				 .append(' ')
				 .append(url)
				 .append(' ')
				 .append(getProtocol())
				 .toString();
	}
	
	public String headersToString() {
		StringBuilder sb = new StringBuilder();
		for(String key : headers.keySet()) {
			String name = key;
			String value = getHeader(name);
			if(value == null) continue;
			sb.append(name)
			  .append(':')
			  .append(' ')
			  .append(value)
			  .append(CRLF);
		}
		if(headers.containsKey("Cookie")) {
			sb.append("Cookie: ")
			  .append(cookiesToString())
			  .append(CRLF);
		}
		return sb.toString();
	}
	
	private String cookiesToString() {
		if(cookies.size() == 0) return "";
		StringBuilder sb = new StringBuilder();
		int i = 0;
		for(String key : cookies.keySet()) {
			String value = cookies.get(key);
			sb.append(key)
			  .append('=')
			  .append(value);
			if(i != cookies.size() - 1) {
				sb.append(';')
				  .append(' ');
			}
			i++;
		}
		return sb.toString();
	}
	
	/**
	 * request a inputstream that contains all http infomation
	 * @return
	 */
	public InputStream toHttpInputStream() {
		StringBuilder sb = new StringBuilder();
		sb.append(messageHeadersToString())
		  .append(CRLF)
		  .append(headersToString())
		  .append(CRLF);
		return new MergedInputStream(bodyInput, sb.toString());
	}

	public void setHeaders(LinkedHashMap<String, String> headers) {
		this.headers = headers;
	}

	public void setMessageHeader(String messageHeader) {
		this.messageHeader = messageHeader;
	}

	public void setBodyInput(InputStream bodyInput) {
		this.bodyInput = bodyInput;
	}
	
	public void addHeader(String name, String value) {
		headers.put(name, value);
	}
	
	public void removeHeader(String name) {
		if("Cookie".equals(name)) return;
		headers.remove(name);
	}
	
	public void addCookie(String name, String value) {
		cookies.put(name, value);
	}
	
	public void removeCookie(String name) {
		cookies.remove(name);
	}
	
	public String getCookie(String name) {
		return cookies.get(name);
	}
	
	public Set<String> cookieKeySet() {
		return cookies.keySet();
	}
	
	public Set<String> headKeySet() {
		Set<String> set = headers.keySet();
		LinkedHashSet<String> keys = new LinkedHashSet<String>();
		for(String key : set) {
			if(!"Cookie".equals(key)) {
				keys.add(key);
			}
		}
		return keys;
	}

	@Override
	public void close() throws IOException {
		bodyInput.close();
	}
	
	public String toLogString() throws UnknownHostException {
		return "Request from " + getClientHost() + " with message header : " + messageHeader + " will probably send to host " + getHostAddress().getHostAddress() + ":" + getPort();
	}
}